#include "graphics.h"
#include "log.h"
#include "sdl\sdl.h"
#include "graphics_glext.h"
#include "sdl\sdl_opengl.h"

#define BUFFER_OFFSET(n) ((char*)NULL + n)

////////////////////////////////////////////////////////////////////////////////

struct GraphicsState
{
  Dimension screen_dim;
  Material std_material;
  VertexCache vertex_cache;
};

static GraphicsState state;

////////////////////////////////////////////////////////////////////////////////

VertexType VertexP2C::type = {false, true, false};
VertexType VertexP2T2::type = {false, false, true};

////////////////////////////////////////////////////////////////////////////////

void Graphics_init()
{
  state.screen_dim.set(0, 0);
}

void Graphics_quit()
{
  Graphics_deleteVertexCache(&state.vertex_cache);
}

bool Graphics_setScreen(const char *caption,int width,int height,bool fullscreen,bool vSync)
{
  SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
  SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
  SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL,vSync ? 1 : 0);
  
  putenv("SDL_VIDEO_CENTERED=center");
  
  if (!SDL_SetVideoMode(width, height, 0, SDL_OPENGL | (fullscreen ? SDL_FULLSCREEN : 0)))
  {
    fprintf(stderr, "Failed to set video mode: %s\n", SDL_GetError());
    
    return false;
  }
  
  SDL_WM_SetCaption(caption,NULL);
  
  state.screen_dim.w = width;
  state.screen_dim.h = height;
  
  GraphicsGLExt_loadFuncs();
  
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity ();
  glOrtho (0, width, height, 0, 0, 1);
  glMatrixMode (GL_MODELVIEW);
  
  Graphics_initMaterial(&state.std_material);
  Graphics_createVertexCache(&state.vertex_cache, 1024 * 1024, 512 * 1024);
  
  return true;
}

const Dimension& Graphics_getScreenDim()
{
  return state.screen_dim;
}

void Graphics_setClearColour(Colour colour)
{
  const float rec_255 = 1.0f / 255.0f;
  
  glClearColor(rec_255 * colour.r, rec_255 * colour.g, rec_255 * colour.b,
      rec_255 * colour.a);
}

void Graphics_clearScreen(bool colour, bool depth, bool stencil)
{
  unsigned mask = 0;
  
  mask |= colour ? GL_COLOR_BUFFER_BIT : 0;
  mask |= depth ? GL_DEPTH_BUFFER_BIT : 0;
  mask |= stencil ? GL_STENCIL_BUFFER_BIT : 0;
  
  glClear(mask);
}

void Graphics_updateScreen()
{
  SDL_GL_SwapBuffers();
}

void Graphics_beginDrawing()
{
  
}

void Graphics_endDrawing()
{
  
}

unsigned Graphics_getVertexSize(const VertexType& vertex_type)
{
  unsigned size = 0;
  
  size += vertex_type.pos3 ? sizeof(Vector3) : sizeof(Vector2);
  size += vertex_type.colour ? sizeof(Colour3) : 0;
  size += vertex_type.tex ? sizeof(Vector2) : 0;
  
  return size;
}

void Graphics_createTexture(Texture* texture, const Image& image)
{
  glGenTextures(1, &texture->id);
  glBindTexture(GL_TEXTURE_2D, texture->id);
  glTexImage2D(GL_TEXTURE_2D, 0, 4, image.width, image.height, 0, GL_RGBA,
    GL_UNSIGNED_BYTE, image.colour);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}

void Graphics_createTexture(Texture* texture, const ImageAlpha& image)
{
  glGenTextures(1, &texture->id);
  glBindTexture(GL_TEXTURE_2D, texture->id);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, image.width, image.height, 0,
    GL_ALPHA, GL_UNSIGNED_BYTE, image.alpha);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}

void Graphics_deleteTexture(Texture* texture)
{
  glDeleteTextures(1, &texture->id);
  
  texture->id = 0;
}

void Graphics_initMaterial(Material* material)
{
  material->colour = Colour::WHITE;
  material->alpha_blend = false;
  material->texture.id = 0;
}

void Graphics_setMaterial(const Material& material)
{
  glColor4ubv(material.colour.elements);
  
  if (material.texture.id)
  {
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, material.texture.id);
  }
  else
  {
    glDisable(GL_TEXTURE_2D);
  }
  
  if (material.alpha_blend)
  {
    glEnable(GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  }
  else
  {
    glDisable(GL_BLEND);
  }
}

bool Graphics_cmpMaterials(const Material& m1, const Material& m2)
{
  if (m1.colour.value != m2.colour.value) return false;
  if (m1.alpha_blend != m2.alpha_blend) return false;
  if (m1.texture.id != m2.texture.id) return false;
  
  return true;
}

void Graphics_createVertexBuffer(VertexBuffer* vb, unsigned size)
{
  glGenBuffers(1, &(vb->id));
  glBindBuffer(GL_ARRAY_BUFFER, vb->id);
  glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_STREAM_DRAW);
  
  vb->vertex_type.pos3 = false;
  vb->vertex_type.colour = false;
  vb->size = size;
  vb->pos = 0;
  vb->buffer = NULL;
}

void Graphics_deleteVertexBuffer(VertexBuffer* vb)
{
  glDeleteBuffers(1, &vb->id);
  
  vb->id = 0;
}

void Graphics_mapVertexBuffer(VertexBuffer* vb, const VertexType& vertex_type)
{
  glBindBuffer(GL_ARRAY_BUFFER, vb->id);
  
  vb->vertex_type.pos3 = vertex_type.pos3;
  vb->vertex_type.colour = vertex_type.colour;
  vb->vertex_type.tex = vertex_type.tex;
  vb->pos = 0;
  vb->buffer = (char*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
}

void Graphics_writeVertexBuffer(VertexBuffer* vb, unsigned length,
  const void* data)
{
  if (vb->pos + length > vb->size)
  {
    return;
  }
  
  memcpy(&vb->buffer[vb->pos], data, length);
  vb->pos += length;
}

void Graphics_unmapVertexBuffer(VertexBuffer* vb)
{
  glUnmapBuffer(GL_ARRAY_BUFFER);
}

void Graphics_drawVertexBuffer(VertexBuffer* vb, unsigned offset, unsigned num)
{
  unsigned vector_size = vb->vertex_type.pos3 ? sizeof(Vector3) : sizeof(Vector2);
  unsigned vertex_size = vector_size;
  
  vertex_size += vb->vertex_type.colour ? sizeof(Colour) : 0;
  vertex_size += vb->vertex_type.tex ? sizeof(Vector2) : 0;
  
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(vb->vertex_type.pos3 ? 3 : 2, GL_FLOAT, vertex_size,
    BUFFER_OFFSET(0));
    
  if (vb->vertex_type.colour)
  {
    glEnableClientState(GL_COLOR_ARRAY);
    glColorPointer(4, GL_UNSIGNED_BYTE, vertex_size,
      BUFFER_OFFSET(vector_size));
  }
  else
  {
    glDisableClientState(GL_COLOR_ARRAY);
  }
  
  if (vb->vertex_type.tex)
  {
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glTexCoordPointer(2, GL_FLOAT, vertex_size, BUFFER_OFFSET(vector_size));
  }
  else
  {
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  }
  
  glDrawArrays(GL_TRIANGLES, offset, num);
}

void Graphics_createIndexBuffer(IndexBuffer* ib, unsigned size)
{
  glGenBuffers(1, &(ib->id));
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib->id);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, NULL, GL_STREAM_DRAW);
  
  ib->size = size;
  ib->pos = 0;
  ib->buffer = NULL;
}

void Graphics_deleteIndexBuffer(IndexBuffer* ib)
{
  glDeleteBuffers(1, &(ib->id));
  
  ib->id = 0;
}

void Graphics_mapIndexBuffer(IndexBuffer* ib)
{
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib->id);
  
  ib->pos = 0;
  ib->buffer = (char*)glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
}

void Graphics_writeIndexBuffer(IndexBuffer* ib, unsigned length,
  const void* data)
{
  if (ib->pos + length > ib->size)
  {
    return;
  }
  
  memcpy(&ib->buffer[ib->pos], data, length);
  ib->pos += length;
}

void Graphics_unmapIndexBuffer(IndexBuffer* ib)
{
  glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
}

void Graphics_pushOrthoView(float left, float right, float top, float bottom)
{
  glMatrixMode (GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity ();
  glOrtho (left, right, bottom, top, 0, 1);
  glMatrixMode (GL_MODELVIEW);
}

void Graphics_popView()
{
  glMatrixMode (GL_PROJECTION);
  glPopMatrix();
  glMatrixMode (GL_MODELVIEW);
}

void Graphics_pushMatrix()
{
  glPushMatrix();
}

void Graphics_multMatrix(const Matrix3x2& m)
{
  float f[16];
  
  f[ 0] = m.a;
  f[ 1] = m.d;
  f[ 2] = 0.0f;
  f[ 3] = 0.0f;
  f[ 4] = m.b;
  f[ 5] = m.e;
  f[ 6] = 0.0f;
  f[ 7] = 0.0f;
  f[ 8] = 0.0f;
  f[ 9] = 0.0f;
  f[10] = 1.0f;
  f[11] = 0.0f;
  f[12] = m.c;
  f[13] = m.f;
  f[14] = 0.0f;
  f[15] = 1.0f;
  
  glMultMatrixf(f);
}

void Graphics_popMatrix()
{
  glPopMatrix();
}

void Graphics_drawBuffers(PrimitiveType primitive_type, VertexBuffer* vb,
  IndexBuffer* ib, unsigned offset, unsigned num)
{
  unsigned gl_primitive_type;
  unsigned vector_size = vb->vertex_type.pos3 ? sizeof(Vector3) : sizeof(Vector2);
  unsigned vertex_size = vector_size;
  
  vertex_size += vb->vertex_type.colour ? sizeof(Colour) : 0;
  vertex_size += vb->vertex_type.tex ? sizeof(Vector2) : 0;
  
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(vb->vertex_type.pos3 ? 3 : 2, GL_FLOAT, vertex_size,
    BUFFER_OFFSET(0));
  
  if (vb->vertex_type.colour)
  {
    glEnableClientState(GL_COLOR_ARRAY);
    glColorPointer(4, GL_UNSIGNED_BYTE, vertex_size,
      BUFFER_OFFSET(vector_size));
  }
  else
  {
    glDisableClientState(GL_COLOR_ARRAY);
  }
  
  if (vb->vertex_type.tex)
  {
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glTexCoordPointer(2, GL_FLOAT, vertex_size, BUFFER_OFFSET(vector_size));
  }
  else
  {
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  }
  
  switch (primitive_type)
  {
    case PRIMITIVE_LINE: gl_primitive_type = GL_LINES; break;
    case PRIMITIVE_LINE_LOOP: gl_primitive_type = GL_LINE_LOOP; break;
    case PRIMITIVE_TRIANGLES: gl_primitive_type = GL_TRIANGLES; break;
    case PRIMITIVE_TRI_FAN: gl_primitive_type = GL_TRIANGLE_FAN; break;
    
    default: gl_primitive_type = GL_POINTS; break;
  }
  
  if (ib)
  {
    glDrawElements(gl_primitive_type, num, GL_UNSIGNED_SHORT, BUFFER_OFFSET(2 * offset));
  }
  else
  {
    glDrawArrays(gl_primitive_type, offset, num);
  }
}

void Graphics_createVertexCache(VertexCache* vc, unsigned vb_size,
  unsigned ib_size)
{
  memset(vc, 0, sizeof(*vc));
  
  Graphics_createVertexBuffer(&vc->vertex_buffer, vb_size);
  Graphics_createIndexBuffer(&vc->index_buffer, ib_size);
}

void Graphics_deleteVertexCache(VertexCache* vc)
{
  Graphics_deleteVertexBuffer(&vc->vertex_buffer);
  Graphics_deleteIndexBuffer(&vc->index_buffer);
}

void Graphics_draw(const VertexType& vertex_type, PrimitiveType primitive_type,
  const Material& material, unsigned num_vertices, const void* vertex,
  unsigned num_indices, const unsigned short* index)
{
  unsigned vertex_size = Graphics_getVertexSize(vertex_type);
  
  Graphics_mapVertexBuffer(&state.vertex_cache.vertex_buffer, vertex_type);
  Graphics_writeVertexBuffer(&state.vertex_cache.vertex_buffer,
    vertex_size * num_vertices, vertex);
  Graphics_unmapVertexBuffer(&state.vertex_cache.vertex_buffer);
  
  if (num_indices> 0 && index)
  {
    Graphics_mapIndexBuffer(&state.vertex_cache.index_buffer);
    Graphics_writeIndexBuffer(&state.vertex_cache.index_buffer,
      2 * num_indices, index);
    Graphics_unmapIndexBuffer(&state.vertex_cache.index_buffer);
  }
  
  Graphics_setMaterial(material);
  
  if (num_indices> 0 && index)
  {
    Graphics_drawBuffers(primitive_type, &state.vertex_cache.vertex_buffer,
      &state.vertex_cache.index_buffer, 0, num_indices);
  }
  else
  {
    Graphics_drawBuffers(primitive_type, &state.vertex_cache.vertex_buffer,
      NULL, 0, num_vertices);
  }
}
